Temel Bir Program Oluşturma, Bölüm 2 - Durum Yönetimi
Özet
- Program durumu, programın kendisinde değil, diğer hesaplarda saklanır.
- Durum, program ID'sinden ve isteğe bağlı tohumlardan türetilen Program Türetilmiş Adres (PDA) hesaplarında saklanır. Bir PDAdaki veriler programcı tarafından tanımlanır.
- Bir hesap oluşturmak, gerekli alanı ve karşılık gelen kirayı lamport cinsinden hesaplamayı gerektirir.
- Yeni bir hesap yaratmak için Sistem Programı üzerindeki
create_account
talimat yöneticisine bir Çapraz Program Çağrısı (CPI) yapılması gerekmektedir. - Bir hesapta veri alanını güncellemek, veriyi hesaba dizi haline getirerek (byte array) dönüştürmeyi içerir.
Ders
Solana, programların durumsuz olmasını sağlayarak hız, verimlilik ve genişletilebilirlik sağlıyor. Durumu programın yürütülebilir dosyası ile birlikte saklamak yerine, programlar Solana'nın hesap modelini kullanarak durumu ayrı PDA hesaplarına okur ve yazar.
Bu model, verileri yönetmek için basit, kullanıcı dostu bir anahtar-değer deposu sağlar ve programların verilerini etkilemeden yükseltilmesine olanak tanır. Ancak, daha eski blok zincirleriyle tanıdık olanlar için bu zorlayıcı olabilir. Bu derste, temel bilgileri öğrenip daha karmaşık blok zincir programlarını adım adım tanıtacağız. Bir Solana programında durum yönetiminin temellerini, durumu bir Rust türü olarak temsil etmeyi, PDA'lar kullanarak hesaplar oluşturmayı ve hesap verilerini dizi haline getirmeyi öğreneceksiniz.
Program Durumu
Tüm Solana hesapları, bir byte dizisi tutan bir veri alanına sahiptir, bu da hesapları bilgisayardaki dosyalar kadar esnek hale getirir. Bir hesapta, gerekli depolama alanı olduğu sürece her şeyi saklayabilirsiniz.
Geleneksel bir dosya sistemi içindeki dosyalar PDF veya MP3 gibi belirli formatlara uygunken, Solana hesabında saklanan verilerin geri getirilip kullanılabilir hale dönüştürülmesi için bir desene uyması gerekir.
Durumu Rust Türü Olarak Temsil Etmek
Rust ile program yazarken, genellikle bu "formatı" tanımlayarak bir Rust veri türü oluştururuz. Bu, önceki derste kesikli talimatları temsil etmek için bir enum oluşturduğumuz gibi benzer bir durumdur
talimat veri dizisini serileştirme dersinin ilk bölümünde
.
Çoğu kullanım durumu için basit bir struct
genellikle yeterlidir. Örnek: Bir not alma programı, notları ayrı hesaplarda saklarken, başlık, gövde ve bir ID için alanlar içerebilir:
struct NoteState {
title: String,
body: String,
id: u64
}
Serileştirme ve Deserileştirme için Borsh Kullanmak
Talimat verisiyle olduğu gibi, Rust veri türümüzü bir byte dizisine dönüştürmemiz ve tersine dönüştürmemiz gerekiyor. Serileştirme, bir nesneyi byte dizisine dönüştürürken, deserileştirme bir nesneyi byte dizisinden yeniden yapılandırır.
Serileştirme ve deserileştirme için Borsh kullanmaya devam edeceğiz. Rust'ta borsh
kütüphanesi, BorshSerialize
ve BorshDeserialize
niteliklerini sağlar. Bu nitelikleri derive
öznitelik makrosu kullanarak uygularız:
use borsh::{BorshSerialize, BorshDeserialize};
#[derive(BorshSerialize, BorshDeserialize)]
struct NoteState {
title: String,
body: String,
id: u64
}
Bu nitelikler, veri serileştirme ve deserileştirme için NoteState
üzerinde yöntemler sağlar.
Hesap Oluşturma
Bir hesabın veri alanını güncellemeden önce, önce hesabı oluşturmalıyız.
Programımızda yeni bir hesap oluşturmak için:
- Hesap için gereken alanı ve kirayı hesaplayın.
- Yeni hesap için bir adres belirleyin.
- Yeni hesabı oluşturmak için sistem programını çağırın.
Alan ve kira
Solana ağı üzerinde veri saklamak, kullanıcıların lamport cinsinden kira ayırmasını gerektirir. Gerekli kira, hesaba ayrılan alan miktarına bağlıdır, bu nedenle hesabı oluşturmadan önce gerekli alanı belirlemeliyiz.
Kiranın daha çok bir depo gibi olduğunu unutmayın; kiraya ayrılan tüm lamportlar bir hesap kapatıldığında tamamen geri alınabilir. Ayrıca, tüm yeni hesaplar kira muaf olmalıdır, yani lamportlar zamanla düşmez. Bir hesap, en az iki yıllık kira tutarını tutuyorsa kira muafı olarak kabul edilir ve bu, hesapların, sahibi hesap kapatıp kira çekene dek sürekli olarak zincirde saklanmasını sağlar.
Not alma uygulamamız örneğinde NoteState
yapılandırması üç alana sahiptir: title
, body
ve id
. Gerekli hesap boyutunu hesaplamak için, her alan için ihtiyaç duyulan alanı toplarız.
Dize gibi dinamik veriler için, Borsh, alanın uzunluğunu saklamak için ek 4 byte ekler. Bu, title
ve body
her biri 4 byte artı kendi boyutlarına ihtiyaç duyduğu anlamına gelir. id
alanı, 64 bitlik bir tamsayıdır veya 8 byte.
Bu uzunlukları ekleyebilir ve solana_program
kütüphanesinin rent
modülündeki minimum_balance
fonksiyonunu kullanarak gereken kirayı hesaplayabiliriz:
// NoteState yapısı için gereken hesap boyutunu hesapla
let account_len: usize = (4 + title.len()) + (4 + body.len()) + 8;
// Gerekli kirayı hesapla
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(account_len);
Program Türetilmiş Adresler (PDA)
Bir hesap oluşturmadan önce, hesaba atamak için bir adrese ihtiyacımız var. Program sahipliğindeki hesaplar için bu, find_program_address
fonksiyonu kullanılarak bulunan bir Program Türetilmiş Adres (PDA) olacaktır.
PDAlar, hesap oluşturan programın adresi olan program ID'si ve isteğe bağlı tohumlar kullanılarak türetilir. find_program_address
fonksiyonu her seferinde aynı girdilerle aynı adresi döndürür, bu da herhangi bir sayıda PDA hesabını belirleyip bulmamızı sağlar.
find_program_address
fonksiyonu ayrıca PDA'nın karşılık gelen bir gizli anahtara sahip olduğundan emin olmak için "bump seed" sağlar. Fonksiyon, geçerli bir PDA bulunana kadar 255'lik bir bump seed ile başlar.
Not alma programımız için, not oluşturucunun halka açık anahtarını ve ID'yi tohum olarak kullanacağız. Bu, her not için hesabı deterministik bir şekilde bulmamıza olanak tanır:
let (note_pda_account, bump_seed) = Pubkey::find_program_address(
&[note_creator.key.as_ref(), id.as_bytes().as_ref(),],
program_id
);
Çapraz Program Çağrısı (CPI)
Kira hesaplayıp geçerli bir PDA türettikten sonra, hesap oluşturmak için Çapraz Program Çağrısı (CPI) kullanarak hesabı oluşturabiliriz. CPI, bir programın başka bir program üzerindeki bir talimatı çağırdığı zaman gerçekleşir. Yeni bir hesap oluşturmak için, sistem programında create_account
talimatını çağıracağız.
CPI'lar ya invoke
ya da invoke_signed
kullanılarak yapılabilir.
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> ProgramResult
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> ProgramResult
Bu derste, bir programa, geleneksel gizli anahtar kullanmadan bir Program Türetilmiş Adres (PDA) için eylemleri yetkilendirebilen invoke_signed
fonksiyonunu keşfedeceğiz. İşte nasıl çalışır:
invoke_signed
, tohumlar, bir bump seed ve program ID'si kullanarak bir PDA türetir.- Bu türetilen PDA, talimattaki tüm hesaplarla karşılaştırılır.
- Eğer bir hesap türetilen PDA ile eşleşirse, o hesabın imzalayıcı alanı true olur.
Bu yöntem, güvenliği sağlar çünkü invoke_signed
, PDA'yı çağıran programın ID'si kullanarak üretir, bu da diğer programların, farklı bir program ID'si ile türetilen hesapları yetkilendiren eşleşen PDAlar üretmesini engeller.
PDA'nın "yetkilendirme" olarak belirtildiğini anlamak önemlidir; bu geleneksel imzalar gibi gizli anahtar kullanmaz. Bunun yerine, bu mekanizma programların kontrol ettikleri PDA hesapları için zincir üzerinde eylemleri onaylamasına olanak tanır.
invoke_signed(
// talimat
&system_instruction::create_account(
note_creator.key,
note_pda_account.key,
rent_lamports,
account_len.try_into().unwrap(),
program_id,
),
// account_infos
&[note_creator.clone(), note_pda_account.clone(), system_program.clone()],
// signers_seeds
&[&[note_creator.key.as_ref(), note_id.as_bytes().as_ref(), &[bump_seed]]],
)?;
Hesap Verilerini Serileştirme ve Deserileştirme
Bir hesap oluşturduktan sonra, verilerini Rust türüne dönüştürerek güncelleyip tekrar serileştirmeliyiz.
Hesap Verilerini Deserileştirmek
Bir hesabın verilerini güncellemek için öncelikle, verilerin byte dizisini hesap türüne dönüştürmeliyiz. Hesabın veri alanını ödünç alarak, mülkiyet almadan ona erişebiliriz. Daha sonra, verileri uygun Rust türüne dönüştürmek için try_from_slice_unchecked()
fonksiyonunu kullanıyoruz:
let mut account_data = try_from_slice_unchecked::<NoteState>(note_pda_account.data.borrow()).unwrap();
account_data.title = title;
account_data.body = rating;
account_data.id = id;
Hesap Verilerini Serileştirmek
Hesabın verilerini temsil eden Rust örneği uygun değerlerle güncellendikten sonra, verileri "kaydedebiliriz".
Bu, oluşturduğunuz Rust türünün örneği üzerinde serialize
fonksiyonu ile yapılır. Hesap verileri için değişken bir referans geçirmenizi gerektirir. Buradaki sözdizimi karmaşık olabilir, bu yüzden tamamen anlamasanız endişelenmeyin. Ödünç alma ve referanslar Rust'ta en zorlu kavramlardan ikisidir.
account_data.serialize(&mut &mut note_pda_account.data.borrow_mut()[..])?;
Yukarıdaki örnek, account_data
nesnesini bir byte dizisine dönüştürür ve note_pda_account
üzerinde data
özelliğine ayarlar. Bu, güncellenmiş account_data
değişkenini yeni hesabın veri alanına kaydeder. Artık bir kullanıcı note_pda_account
'u alıp verileri deserileştirdiğinde, hesabımıza serileştirilmiş güncellenmiş verileri görüntüleyecektir.
İteratörler
Önceki örneklerde note_creator
'a atıfta bulunduğumuzda onun nereden geldiğini göstermediğinizi fark etmiş olabilirsiniz.
Buna ve diğer hesaplara erişmek için bir
İteratör kullanıyoruz. İteratör, bir değerler kümesinin her bir elemanına ardışık erişim sağlamak için kullanılan bir Rust özelliğidir. İteratörler, Solana programlarında, program giriş noktasına accounts
argümanı aracılığıyla geçirilen hesap listesini güvenli bir şekilde yinelemek için kullanılır.
Rust İteratörü
İteratör deseni, bir dizi öğe üzerinde görevler gerçekleştirmenizi sağlar. iter()
yöntemi, bir koleksiyona atıfta bulunan bir iteratör nesnesi oluşturur. Rust'ta iteratörler tembeldir ve tüketen yöntemler çağrılmadıkça hiçbir etkisi yoktur. Ardışık dizide bir sonraki öğeyi almak için next()
fonksiyonunu kullanın.
let v1 = vec![1, 2, 3];
// vec üzerinde iteratörü oluştur
let v1_iter = v1.iter();
// İlk öğeyi almak için iteratörü kullan
let first_item = v1_iter.next();
// İkinci öğeyi almak için iteratörü kullan
let second_item = v1_iter.next();
Solana Hesapları İteratörü
Solana programlarında, talimat yöneticisi, tüm gerekli hesaplar için AccountInfo
öğelerini içeren bir accounts
argümanı alır. Talimat yöneticisi içinde bu hesapları kullanmak için, accounts
'a değişken bir referans ile bir iteratör oluşturun. Bu yaklaşım, hesap bilgilerini ardışık olarak işleyip, talimat yöneticisi mantığı için gereken verilere erişmenizi sağlar.
İteratörü doğrudan kullanmak yerine, solana_program
kütüphanesinde sağlanan account_info
modülünden next_account_info
fonksiyonuna iteratörü geçirebilirsiniz.
Örneğin, bir not alma programında yeni bir not oluşturmayı düşündüğünüzü varsayın. Bu talimat, minimum olarak aşağıdaki hesapları gerektirir:
- Notu oluşturan kullanıcının hesabı.
- Notu saklamak için bir PDA.
- Yeni bir hesabı başlatmak için
system_program
hesabı.
Üç hesap, program giriş noktasına accounts
argümanı aracılığıyla geçirilir. Daha sonra, her hesapla ilişkili AccountInfo
'yu ayırmak için bir accounts
iteratörü kullanılır.
Not: &mut
anahtar kelimesi, accounts
argümanına değişken bir referans olduğunu belirtir. Daha fazla ayrıntı için
Rust'ta referanslar ve mut anahtar kelimesi referanslarına bakabilirsiniz.
// Hesap iteratörünü al
let account_info_iter = &mut accounts.iter();
// Hesapları al
let note_creator = next_account_info(account_info_iter)?;
let note_pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
Laboratuvar
Bu bölüm birkaç yeni kavram tanıttı. Bunları birlikte uygulayalım ve önceki dersten Movie Review programını geliştirelim. Bu dersten başlıyorsanız bile, size ulaşmanızı sağlayacak. Kodumuzu yazmak, oluşturmak ve dağıtmak için Solana Playground kullanacağız.
Kısaca hatırlatmak gerekirse, kullanıcıların filmleri değerlendirmesine olanak tanıyan bir Solana programı oluşturuyoruz. Önceki derste talimat verisini deserileştirdik
ancak bu verileri bir hesapta saklamadık. Şimdi programımızı, kullanıcının film incelemesini saklamak için yeni hesaplar oluşturacak şekilde güncelleyelim.
1. Başlangıç kodunu alın
Geçtiğimiz derste laboratuvarı tam olarak tamamlamadıysanız ya da kaçırdığınız bir şey olup olmadığını kontrol etmek istiyorsanız, başlangıç koduna atıfta bulunabilirsiniz.
Programımız şu anda, program giriş noktasına iletilen instruction_data
'yı deserileştirmek için kullanılan bir instruction.rs
dosyasını içeriyor. lib.rs
dosyasını da deserileştirilmiş talimat verimizi program günlüğüne yazdırabileceğimiz noktaya kadar tamamladık ve msg!
makrosunu kullandık.
2. Hesap verilerini temsil etmek için yapı oluşturun
Şimdi state.rs
adında yeni bir dosya oluşturarak başlayalım.
Bu dosya:
- Yeni bir hesabın veri alanını doldurmak için kullanılan
struct
'ı tanımlayacaktır. - Bu yapıya
BorshSerialize
veBorshDeserialize
niteliklerini ekleyecektir.
Öncelikle borsh
kütüphanesinden gerekli öğeleri içeri aktaralım:
use borsh::{BorshSerialize, BorshDeserialize};
Ardından, her yeni film inceleme hesabının veri alanında saklayacağı parametreleri tanımlayan MovieAccountState
yapısını oluşturalım. Yapı aşağıdaki alanları içerir:
is_initialized
- hesabın başlatılıp başlatılmadığını gösterir.rating
- kullanıcının filmin derecelendirmesidir.description
- kullanıcının film ile ilgili açıklamasıdır.title
- değerlendirilen filmin başlığıdır.
#[derive(BorshSerialize, BorshDeserialize, Default)]
pub struct MovieAccountState {
pub is_initialized: bool,
pub rating: u8,
pub title: String,
pub description: String,
}
3. lib.rs'yi güncelleyin
Sonraki adımda lib.rs
dosyasını güncelleyin. Film Değerlendirme programını tamamlamak için gereken her şeyi içe aktarmaya başlayın. Her birimin detayları için
solana_program kütüphanesine başvurabilirsiniz.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke_signed,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
use borsh::{BorshDeserialize, BorshSerialize};
pub mod instruction;
pub mod state;
use instruction::MovieInstruction;
use state::MovieAccountState;
4. Hesaplar arasında iterasyon yapın
add_movie_review
fonksiyonunu geliştirirken devam edin. Bir dizi hesabın, add_movie_review
fonksiyonuna tek bir accounts
argümanı aracılığıyla geçirildiğini hatırlayın. Talimatı işlemek için, accounts
üzerinde yineleme yapın ve her bir hesap için AccountInfo
'yu bir değişkene atayın.
// Hesap iteratörünü al
let account_info_iter = &mut accounts.iter();
// Hesapları al
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
5. PDA türetin
add_movie_review
fonksiyonu içinde, kullanıcının geçmesini beklediğiniz PDA'yı türetin. pda_account
aynı hesaba referans vermeye devam etse de, find_program_address()
fonksiyonunu çağırmanız gerektiği için bump seed'e ihtiyaç vardır.
Her yeni hesabın PDA'sı, initialize edenin halka açık anahtarı ve film başlığı tohumları kullanılarak türetilir. Bu ayarlama, her kullanıcının sadece bir film başlığı için bir incelemesiyle sınırlandırır, ancak farklı kullanıcıların aynı filmi değerlendirmesine ve aynı kullanıcının farklı filmleri değerlendirmesine olanak tanır.
// PDA'yı türet
let (pda, bump_seed) = Pubkey::find_program_address(
&[initializer.key.as_ref(), title.as_bytes().as_ref()],
program_id,
);
6. Alan ve kira hesaplayın
Yeni hesap için gereken kirayı hesaplayın. Kira, bir kullanıcının Solana ağında veri saklamak için bir hesaba ayırması gereken lamport miktarıdır. Kira hesaplamak için önce yeni hesabın gerektirdiği alanı belirleyin.
MovieAccountState
yapılandırması dört alana sahiptir. rating
ve is_initialized
için 1 byte ayıracağız. title
ve description
için ise uzunluğuna 4 byte daha ekleyeceğiz.
// Hesap boyutunu hesapla
let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
// Gerekli kirayı hesapla
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(account_len);
7. Yeni hesap oluşturun
Kira hesaplandıktan ve PDA doğrulandığında, yeni hesabı oluşturun. Bunu yapmak için, sistem programından create_account
talimatını Cross Program Invocation (CPI) kullanarak çağırın. Hesap, bir PDA ile oluşturulduğundan ve Film İnceleme programı talimatları "imzalamak" zorunda olduğundan invoke_signed
kullanmalısınız.
// Hesabı oluştur
invoke_signed(
&system_instruction::create_account(
initializer.key,
pda_account.key,
rent_lamports,
account_len.try_into().unwrap(),
program_id,
),
&[
initializer.clone(),
pda_account.clone(),
system_program.clone(),
],
&[&[
initializer.key.as_ref(),
title.as_bytes().as_ref(),
&[bump_seed],
]],
)?;
msg!("PDA oluşturuldu: {}", pda);
8. Hesap verilerini güncelleyin
Artık yeni hesabı oluşturduğumuza göre, state.rs
dosyamızdaki MovieAccountState
yapısının formatını kullanarak yeni hesabın veri alanını güncellemeye hazırız. Öncelikle, pda_account
'taki hesap verilerini try_from_slice
kullanarak deserileştirin ve ardından her alanın değerlerini ayarlayın.
msg!("Durum hesabını açıyoruz");
let mut account_data =
MovieAccountState::try_from_slice(&pda_account.data.borrow())
.unwrap_or(MovieAccountState::default());
msg!("Hesap verileri ödünç alındı");
account_data.title = title;
account_data.rating = rating;
account_data.description = description;
account_data.is_initialized = true;
Son olarak, güncellenmiş account_data
'yı pda_account
ın veri alanına serileştirin.
msg!("Hesabı serileştiriyoruz");
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
msg!("Durum hesabı serileştirildi");
9. İnşa et ve dağıt
Artık programınızı inşa edip dağıtmaya hazırsınız!
Programınızı doğru instructiondata
ile bir işlem göndererek test edebilirsiniz. Bunun için
barkod veya
frontend oluşturduğumuz şeylerden birini kullanabilirsiniz.
Özelleştirilmiş Talimat Verilerini Deserialize Etme dersi
içerisinde her iki durumda da, doğru programı test ettiğinizden emin olmak için program ID'sini ilgili web/components/ui/review-form.ts
dosyasına ayarlayın.
Eğer barkodu kullanıyorsanız, sadece
index.ts
bileşenindeki movieProgramId
'ye atanan değeri, dağıttığınız programın genel anahtarı ile değiştirin.
Eğer frontend'i kullanıyorsanız, sadece review-form.tsx
bileşenindeki
MOVIE_REVIEW_PROGRAM_ID
'yi, dağıttığınız programın adresi ile değiştirin.
Sonrasında frontend'i çalıştırın, bir görüntü gönderin ve incelemeyi görmek için tarayıcıyı yenileyin.
Bu projede bu kavramlarla rahat hissetmek için daha fazla zamana ihtiyacınız varsa, devam etmeden önce
çözüm koduna bir göz atın.
Meydan Okuma
Şimdi bağımsız olarak bir şeyler inşa etme sırası sizde. Bu derste tanıtılan kavramlarla,
şimdi Modul 1'deki Öğrenci Tanıtım programını yeniden yaratmak için ihtiyacınız olan her şeye sahipsiniz.
Öğrenci Tanıtım programı, öğrencilerin kendilerini tanıttıkları bir Solana Programıdır. Program, bir kullanıcının ismini ve kısa bir mesajı
instruction_data
olarak alır ve verileri onchain'da saklamak için bir hesap oluşturur.
— Dökümantasyon
Bu derste öğrendiklerinizi kullanarak bu programı oluşturun. Programın ayrıca bir isim ve kısa bir mesajı instruction data
olarak almasının yanı sıra:
- Her öğrenci için ayrı bir hesap oluşturmalıdır.
- Her hesapta
is_initialized
'ı boolean,name
'i string vemsg
'yi de string olarak saklamalıdır.
Programınızı test etmek için, oluşturduğumuz
frontend ile inşa edin
Sayfa, Sıralama ve Filtre Program Verileri dersi
.
Ön yüzdeki program ID'sini, dağıttığınız programla değiştirin.
Eğer yapabilirseniz bu işi bağımsız yapmaya çalışın! Ama takılırsanız,
çözüm koduna başvurabilirsiniz.
Kodunuzu GitHub'a yükleyin ve
bize bu ders hakkında ne düşündüğünüzü söyleyin!